Skip to content

feat(worker): run the operator console as a service (#708) - #728

Merged
edgehero merged 1 commit into
mainfrom
feat/console-service-708b
Oct 21, 2026
Merged

edgehero merged 1 commit into
mainfrom
feat/console-service-708b

Conversation

@edgehero

Copy link
Copy Markdown
Owner

Part of #708. pi-dispatch service install --console keeps the web console running across reboots.

What changes

  • A third service component. The console sits beside the worker and --receiver (the two flags cannot be combined). It installs as a systemd user unit or a staged system unit, a launchd agent (com.pi-dispatch.console), or an nssm service, and runs node <console.mjs> serve --deployment <folder>.
  • Finding the console. The console comes from the admin package installed beside the worker. A pi-dispatch-console on PATH counts only if it resolves to the package's own dist/console.mjs, so pnpm and Volta shims are refused.
  • Install refuses when the admin package is missing, or when the tokens file has no operator token (serve would exit 2 on every start).
  • The console never loads the whole .env. console.service has no EnvironmentFile=, and the plist and nssm entry set PI_WRAPPER_NO_DOTENV=1, which both wrappers check before loading .env. serve --deployment reads only its own keys, and still refuses an untrusted .env.
  • Folder quoting. ExecStart words are quoted the systemd way, and the plist escapes & < >. A folder holding a space, %, $, quotes or ; renders and reads back as itself. Ordinary worker and receiver renders are byte-identical to before.
  • One health probe rule in worker/src/console-probe.mjs, which the admin's /dispatch console will import. It contacts only loopback or the console's socket.
  • Doctor reports an installed console unit and whether the console answers.
  • up suggests the install when an operator token exists and no unit does.
  • --drain is refused for anything but the worker.

Specs

Amended: DES-OPERATOR-CONSOLE, DES-SERVICE-ENV-SETUP-SEAM, INT-CONSOLE-TOKENS-FILE-CONTRACT, REQ-DEPLOYMENT-BOOTSTRAP.

Residuals

  • Stale lock after a crash. If the console crashes and a new process takes its old pid, the stale lock makes serve exit 2 on every start, and the unit stays stopped. The fix line names the lock path.
  • Re-rendered units change bytes. After an upgrade, a re-rendered unit whose path holds characters outside the bare class gets new bytes. Both forms are valid.
  • Windows refuses " % & | < > ^ in any command word.

Verification

  • Review: two adversarial rounds.
  • Tests: service, doctor, up, env-file and publish all pass, and a test-side systemd unquoter checks that the decoded argv matches the intended one.
  • Renders: worker and receiver are byte-identical to main.
  • The real systemd parse is checked only by deploy-lint (systemd-analyze verify), which isn't installed locally.
  • Not yet run: a launchd or systemd install on a real host.

Part of #708. `pi-dispatch service install --console` keeps the web console
running across reboots, so nobody has to keep a terminal open for
`pi-dispatch-console serve`.

- The console is a third service component beside the worker and
  --receiver (not together with --receiver): a systemd user or staged
  system unit, a launchd agent (com.pi-dispatch.console), or an nssm
  service. It runs `node <console.mjs> serve --deployment <folder>`.
- The console is found through the admin package beside the worker. A
  `pi-dispatch-console` on PATH counts only if it resolves to the
  package's own dist/console.mjs, so shims are refused. Install refuses
  when it is missing, or when the tokens file has no operator token,
  since serve would then exit 2 on every start.
- The console never loads the whole .env: console.service has no
  EnvironmentFile=, and its plist and nssm entry set
  PI_WRAPPER_NO_DOTENV=1, which both wrappers check before the load.
  serve --deployment reads its own keys and still refuses an untrusted
  .env.
- ExecStart words are quoted the systemd way, so a folder holding a
  space, %, $, quotes or ; renders and reads back as itself. The plist
  escapes & < >, and --env-setup moves a non-plain argv after the script
  as separate words. Ordinary worker and receiver renders are
  byte-identical to before.
- New worker/src/console-probe.mjs (`./console-probe`) is the one
  healthz probe rule. It contacts only loopback or the console's socket,
  never follows a redirect, and gives up after 1 s.
- Doctor reports an installed console unit and whether the console
  answers. It says nothing for a deployment with no console configured.
- `up` suggests the install when an operator token exists and no unit
  does. --drain is refused for anything but the worker.
- deploy/console.service is new (mirrored in worker/deploy). deploy-lint
  verifies it and a rendered special-path unit.

Specs: DES-OPERATOR-CONSOLE (the console as a service),
DES-SERVICE-ENV-SETUP-SEAM, INT-CONSOLE-TOKENS-FILE-CONTRACT and
REQ-DEPLOYMENT-BOOTSTRAP are amended.

Signed-off-by: Rob Boerman <robboerman@live.nl>
@edgehero
edgehero merged commit c7d054e into main Oct 21, 2026
10 of 11 checks passed
edgehero added a commit that referenced this pull request Oct 21, 2026
…console (#708)

Part of #708. An operator in pi types `/dispatch console` and gets the
console's address, whether it is running and, with --link, a one-time
sign-in link, all in one step.

- `/dispatch console [page] [--link [name]] [--no-open]` is
  operator-typed only. Its output goes to the operator's notice, never
  to model context, and no tool exposes it. `page` is runs, fleet,
  insights, config, parked, audit or a run id. The address follows
  consolePageLines' rules.
- Running or not: the worker's console probe (#728, the one rule doctor
  uses) asks /healthz on loopback or the socket only. "Not running"
  names `pi-dispatch-console serve` and `service install --console`.
- --link mints a 60 s, single-use login code in-process for an operator
  token (asking which when there are several). It appends a `panel`
  audit row first, and pairs it with a `failed` row under the same
  ticket if the mint is refused. It mints only when the console answers
  as running, only in pi's TUI mode, and only where the codes directory
  is guarded. The link can carry a return page (`#<code>&next=/runs`),
  judged by the server and by the page script against the console's own
  pages.
- On a local session (no SSH, a display, a TCP listener) the link is
  handed to the browser through a 0600 page in a 0700 temporary
  directory, so the code never appears in a process's arguments. The URL
  is always printed, and --no-open only prints. On Windows the opener
  uses url.dll so a URL holding & survives.
- The login-codes directory joins the guarded files, as the tokens file
  is, judged by identity even before it exists. The console also refuses
  a code file that another account can read or owns, and one whose
  expiry exceeds the code's lifetime.

Specs: REQ-ADMIN-VIA-PI-EXTENSION and DES-ADMIN-VIA-PI-EXTENSION (the
subcommand; a browser only for it on a local session), OQ-024 (reopened
and closed with the local-session rule), DES-OPERATOR-CONSOLE (the panel
as a second minter, the return-path rule, threat 4 covering the login
codes) and INT-CONSOLE-AUDIT-LOG are amended. UNCHANGED, checked: "binds
no network port", the model-callable tool list and REQ-CONSOLE-SECURITY.

Signed-off-by: Rob Boerman <robboerman@live.nl>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant